﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace BioCompEx3
{
    //Artificial neural network
    class ANN
    {
        private DB dbInputDB;        
        public ArrayList alNeurons;

        public int iInputNeuronsAmount;
        public int iFirstLayerNeuronsAmmount;
        public int iSecondLayerNeuronsAmmount;
        private double alpha;  

        /*Constructor
         */
        public ANN(DB inputDB, int inputNeuronsAmount, int iFirstLayerAmmount, int iSecondLayerAmmount, double dAlpha)
        {
            dbInputDB = inputDB;            
            alNeurons = new ArrayList();
            iInputNeuronsAmount = inputNeuronsAmount;
            iFirstLayerNeuronsAmmount = iFirstLayerAmmount;
            iSecondLayerNeuronsAmmount = iSecondLayerAmmount;
            alpha = dAlpha;
        }

        /*Perform the ANN process including backpropagation
         * 
         * Returns - The ammount of correct validation tupples
         */ 
        public long performLearning()
        {
            //Create all neurons
            createInitialNeuralNetwork();

            //Perform learning
            teachNeuralNetwork();

            //Check the learning phase - validation
            return validateNeuralNetwork();
        }

        //Generate the first random ANN
        private void createInitialNeuralNetwork()
        {
            int iIndex = 0;
            //Create the input neurons
            for (iIndex = 0; iIndex < iInputNeuronsAmount; iIndex++)
            {
                //Create an input neuron
                alNeurons.Add(new Neuron(0, iIndex,NeuronType.inputNeuron));
            }

            //Create the first layer of middle neurons
            for (; iIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount); iIndex ++)
            {
                //Create a middle layer neuron
                alNeurons.Add(new Neuron(0, iIndex, NeuronType.middleNeuron));
                //Create edges from each of the input neurons to this neuron
                for (int i=0; i < iInputNeuronsAmount; i++)
                {
                    //Add an edge with random weights
                    ((Neuron)(alNeurons[i])).eEdges.Add(new Edge(iIndex));
                }
            }            

            //Create the second layer of middle neurons
            for (; iIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount + iSecondLayerNeuronsAmmount); iIndex++)
            {
                //Create a middle layer neuron
                alNeurons.Add(new Neuron(0, iIndex, NeuronType.middleNeuron));
                //Create edges from each of the first layer neuron to the second layer neuron
                for (int i=iInputNeuronsAmount; i < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount); i++)
                {
                    //Add an edge with random weights
                    ((Neuron)(alNeurons[i])).eEdges.Add(new Edge(iIndex));
                }
            }

            //Create the last neuron
            alNeurons.Add(new Neuron(0, iIndex, NeuronType.outputNeuron));
            //Create edges from the second layer to the output neuron
            for (int i = (iInputNeuronsAmount + iFirstLayerNeuronsAmmount); i < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount + iSecondLayerNeuronsAmmount); i++)
            {
                //Add an edge with random weight
                ((Neuron)(alNeurons[i])).eEdges.Add(new Edge(iIndex));
            }

            iIndex++;
            //Create a bias neuron
            alNeurons.Add(new Neuron(1, iIndex, NeuronType.biasNeuron));
            //Add edges from the biased neuron to all of the first layer
            for (int i = (iInputNeuronsAmount); i < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount); i++)
            {
                //Create the edge
                ((Neuron)(alNeurons[iIndex])).eEdges.Add(new Edge(i));
            }         
        }

        /*Uses the learning set to perform back propegation
         */
        private void teachNeuralNetwork()
        {   
            //Perform learning as long as there are learning tupples
            Tupple tCurrentTupple;
            while ((tCurrentTupple = dbInputDB.getNextLearningTupple()) != null)
            {
                //Populate the input neurons with the current input
                for (int i = 0; i < iInputNeuronsAmount; i++)
                {
                    if ((tCurrentTupple.Result[i] != 1) && (tCurrentTupple.Result[i] != 0))
                    {
                        throw (new Exception("Tupple result array must contain only 1 or 0"));
                    }
                    if (((Neuron)alNeurons[i]).ntType == NeuronType.inputNeuron)
                        ((Neuron)alNeurons[i]).dOutputValue = tCurrentTupple.Result[i];
                }

                //Perform back propagation
                performBackPropagation(tCurrentTupple);
            }
        }

        /*Performs the neural network's back propagation
         * 
         */
        private void performBackPropagation(Tupple tCurrentTupple)
        {
            //Get the current network's output
            double dNeuralNetworkOutput = calculateOutputForCurrentInput();
            
            //Get the expected output
            double dCorrectOutput = tCurrentTupple.IsProfitable;

            //Calculate the error
            double dError = dCorrectOutput - dNeuralNetworkOutput;

            //Calculate the correct neuron value
            double dCorrectNeuronValue = dError * dNeuralNetworkOutput * (1 - dNeuralNetworkOutput);
            
            //Update the output neuron with the corrected output 
            ((Neuron)alNeurons[alNeurons.Count - 2]).dOutputValue = dCorrectNeuronValue;

            /***************************
             *Right to left calculations 
             ***************************/

            //Calculate correct neuron values for the second layer
            for (int iNeuronIndex = (iInputNeuronsAmount + iFirstLayerNeuronsAmmount);
                iNeuronIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount + iSecondLayerNeuronsAmmount);
                iNeuronIndex++)
            {
                //For each of the layer's neurons
                double dSumOfWeightedCorrectValues = 0;
                for (int iEdgeIndex = 0;
                    iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count;
                    iEdgeIndex++)
                {
                    double dCurrentEdgeWeight = ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).dWeight;
                    int iTargetNeuronIndex = ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).iTargetNeuronIndex;
                    //Sum up the weighted correct output
                    dSumOfWeightedCorrectValues += dCurrentEdgeWeight * ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue;
                }
                //Update the current neuron output with the corrected output
                ((Neuron)alNeurons[iNeuronIndex]).dOutputValue = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue * (1 - ((Neuron)alNeurons[iNeuronIndex]).dOutputValue) * dSumOfWeightedCorrectValues;
            }

            //Calculate correct neuron values for the first layer
            for (int iNeuronIndex = (iInputNeuronsAmount);
                iNeuronIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount);
                iNeuronIndex++)
            {
                //For each of the layer's neurons
                double dSumOfWeightedCorrectValues = 0;
                for (int iEdgeIndex = 0;
                    iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count;
                    iEdgeIndex++)
                {
                    double dCurrentEdgeWeight = ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).dWeight;
                    int iTargetNeuronIndex = ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).iTargetNeuronIndex;

                    dSumOfWeightedCorrectValues += dCurrentEdgeWeight * ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue;
                }
                //Update the current neuron output with the corrected output
                ((Neuron)alNeurons[iNeuronIndex]).dOutputValue = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue * (1 - ((Neuron)alNeurons[iNeuronIndex]).dOutputValue) * dSumOfWeightedCorrectValues;
            }

            /***************************
             *Left to right caculations
             ***************************/

            //double alpha = 0.6;

            //Input layer calculations
            for (int iNeuronIndex = 0;
                iNeuronIndex < (iInputNeuronsAmount);
                iNeuronIndex++)
            {
                //For each neuron recalculate the edges weights
                //Go over the neuron's edges
                for (int iEdgeIndex = 0; 
                    iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count; 
                    iEdgeIndex++)
                {
                    //Weight = alpha * out * correct out
                    double dCurrentNeuronOutput = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue;
                    int iTargetNeuronIndex = ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).iTargetNeuronIndex;
                    double dTargetNeuronCorrectOutput = ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue;

                    ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).dWeight += alpha * dCurrentNeuronOutput * dTargetNeuronCorrectOutput;
                }
            }

            //Bias neuron - recalculate the edges weights
            int iBiasNeuronIndex = alNeurons.Count - 1;
            for (int iEdgeIndex = 0;
                    iEdgeIndex < ((Neuron)alNeurons[iBiasNeuronIndex]).eEdges.Count;
                    iEdgeIndex++)
            {
                //Weight = alpha * out * correct out
                double dCurrentNeuronOutput = ((Neuron)alNeurons[iBiasNeuronIndex]).dOutputValue;
                int iTargetNeuronIndex = ((Edge)((Neuron)alNeurons[iBiasNeuronIndex]).eEdges[iEdgeIndex]).iTargetNeuronIndex;
                double dTargetNeuronCorrectOutput = ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue;

                ((Edge)((Neuron)alNeurons[iBiasNeuronIndex]).eEdges[iEdgeIndex]).dWeight += alpha * dCurrentNeuronOutput * dTargetNeuronCorrectOutput;
            }

            //First layer calculations
            for (int iNeuronIndex = (iInputNeuronsAmount);
                iNeuronIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount);
                iNeuronIndex++)
            {
                //For each neuron recalculate the edges weights
                //Go over the neuron's edges
                for (int iEdgeIndex = 0;
                    iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count;
                    iEdgeIndex++)
                {
                    //Weight = alpha * out * correct out
                    double dCurrentNeuronOutput = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue;
                    int iTargetNeuronIndex = ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).iTargetNeuronIndex;
                    double dTargetNeuronCorrectOutput = ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue;

                    ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).dWeight += alpha * dCurrentNeuronOutput * dTargetNeuronCorrectOutput;
                }
            }

            //Second layer calculations
            for (int iNeuronIndex = (iInputNeuronsAmount + iFirstLayerNeuronsAmmount);
                iNeuronIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount + iSecondLayerNeuronsAmmount);
                iNeuronIndex++)
            {
                //For each neuron recalculate the edges weights
                //Go over the neuron's edges
                for (int iEdgeIndex = 0;
                    iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count;
                    iEdgeIndex++)
                {
                    //Weight = alpha * out * correct out
                    double dCurrentNeuronOutput = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue;
                    int iTargetNeuronIndex = ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).iTargetNeuronIndex;
                    double dTargetNeuronCorrectOutput = ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue;

                    ((Edge)((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex]).dWeight += alpha * dCurrentNeuronOutput * dTargetNeuronCorrectOutput;
                }
            }
        }

        /* Calculates the output for the current input
         */
        private double calculateOutputForCurrentInput()
        {
            //Zero out all neurons
            zeroAllNeurons();

            //Perform input layer calculations
            //Add all outputs to the neurons
            for (int iNeuronIndex = 0; iNeuronIndex < iInputNeuronsAmount; iNeuronIndex++)
            {
                double dCurrentNeuronOutput = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue;

                //For each neuron add its output to the destination neurons
                for (int iEdgeIndex = 0; iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count; iEdgeIndex++)
                {
                    int iTargetNeuronIndex = ((Edge)(((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex])).iTargetNeuronIndex;
                    double dCurrentEdgeWeight = ((Edge)(((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex])).dWeight;                    

                    ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue += dCurrentNeuronOutput * dCurrentEdgeWeight;
                }
            } 

            //Add the bias neuron output to the first layer
            int iBiasIndex = alNeurons.Count - 1;
            for (int iEdgeIndex = 0; iEdgeIndex < ((Neuron)alNeurons[iBiasIndex]).eEdges.Count; iEdgeIndex++)
            {
                int iTargetNeuronIndex = ((Edge)(((Neuron)alNeurons[iBiasIndex]).eEdges[iEdgeIndex])).iTargetNeuronIndex;
                double dCurrentEdgeWeight = ((Edge)(((Neuron)alNeurons[iBiasIndex]).eEdges[iEdgeIndex])).dWeight;
                double dCurrentNeuronOutput = ((Neuron)alNeurons[iBiasIndex]).dOutputValue;

                ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue += dCurrentNeuronOutput * dCurrentEdgeWeight;
            }

            //Perform first layer calculations
            for (int iNeuronIndex = iInputNeuronsAmount; iNeuronIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount); iNeuronIndex++)
            {
                //Calculate the sigmoid output value
                ((Neuron)alNeurons[iNeuronIndex]).dOutputValue = sigmoidFunction(((Neuron)alNeurons[iNeuronIndex]).dOutputValue);
                double dCurrentNeuronOutput = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue;                

                //For each neuron add its output to the destination neurons
                for (int iEdgeIndex = 0; iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count; iEdgeIndex++)
                {
                    int iTargetNeuronIndex = ((Edge)(((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex])).iTargetNeuronIndex;
                    double dCurrentEdgeWeight = ((Edge)(((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex])).dWeight;

                    ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue += dCurrentNeuronOutput * dCurrentEdgeWeight;
                }
            }   

            //Perform second layer calculations            
            for (int iNeuronIndex = (iInputNeuronsAmount + iFirstLayerNeuronsAmmount); 
                iNeuronIndex < (iInputNeuronsAmount + iFirstLayerNeuronsAmmount + iSecondLayerNeuronsAmmount); 
                iNeuronIndex++)
            {
                //Calculate the sigmoid output value
                ((Neuron)alNeurons[iNeuronIndex]).dOutputValue = sigmoidFunction(((Neuron)alNeurons[iNeuronIndex]).dOutputValue);
                double dCurrentNeuronOutput = ((Neuron)alNeurons[iNeuronIndex]).dOutputValue;     

                //For each neuron add its output to the destination neurons
                for (int iEdgeIndex = 0; iEdgeIndex < ((Neuron)alNeurons[iNeuronIndex]).eEdges.Count; iEdgeIndex++)
                {
                    int iTargetNeuronIndex = ((Edge)(((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex])).iTargetNeuronIndex;
                    double dCurrentEdgeWeight = ((Edge)(((Neuron)alNeurons[iNeuronIndex]).eEdges[iEdgeIndex])).dWeight;                    

                    ((Neuron)alNeurons[iTargetNeuronIndex]).dOutputValue += dCurrentNeuronOutput * dCurrentEdgeWeight;
                }
            }   

            //Calculate output
            int iOutputNeuronIndex = alNeurons.Count - 2;
            double dOutputValue = sigmoidFunction(((Neuron)alNeurons[iOutputNeuronIndex]).dOutputValue);
            
            return dOutputValue;
        }

        /* Sets all of the Neuron valus to 0 (Except of the Bias)
         */
        private void zeroAllNeurons()
        {
            for (int i = 0; i < alNeurons.Count; i++)
            {
                if (((Neuron)alNeurons[i]).ntType != NeuronType.inputNeuron)
                {
                    if (((Neuron)alNeurons[i]).ntType != NeuronType.biasNeuron)
                        ((Neuron)alNeurons[i]).dOutputValue = 0;
                    else
                        ((Neuron)alNeurons[i]).dOutputValue = 1;
                }
            }
        }

        /* Calculate the sigmoid function 1 / (1 + e ^ -input)
         */
        private double sigmoidFunction(double valueToComput)
        {
            return (1/(1 + Math.Pow(Math.E, - valueToComput)));
        }

        /*Checks how many tupples are identified correctly by the NN
         * 
         * Returns - Amount of tupples that were identified correctly
         */ 
        public long validateNeuralNetwork()
        {
            long lNumberOfCorrectResults = 0;            

            //Zero out all neurons outputs
            zeroAllNeurons();

            //Perform validation as long as there are learning tupples
            Tupple tCurrentTupple;
            while ((tCurrentTupple = dbInputDB.getNextValidationTupple()) != null)
            {
                //Populate the input neurons with the current tupple input
                for (int i = 0; i < iInputNeuronsAmount; i++)
                {
                    if ((tCurrentTupple.Result[i] != 1) && (tCurrentTupple.Result[i] != 0))
                    {
                        throw (new Exception("Tupple result array must contain only 1 or 0"));
                    }
                    if (((Neuron)alNeurons[i]).ntType == NeuronType.inputNeuron)
                        ((Neuron)alNeurons[i]).dOutputValue = tCurrentTupple.Result[i];
                }

                //Compare expected result to actual result
                double dActualResult = calculateOutputForCurrentInput();
                int iNormalizedResult= normalizeOutputToBinaryInt(dActualResult);
                if (tCurrentTupple.IsProfitable == iNormalizedResult)
                    lNumberOfCorrectResults++;
            }

            return lNumberOfCorrectResults;
        }

        /*Returns an array of results for each of the test tupples
         */ 
        public ArrayList performTest()
        {
            ArrayList alResultsArray = new ArrayList();            

            //Zero out all neurons outputs
            zeroAllNeurons();

            //Perform validation as long as there are learning tupples
            Tupple tCurrentTupple;
            while ((tCurrentTupple = dbInputDB.getNextTestTupple()) != null)
            {
                //Populate the input neurons with the current tupple input
                for (int i = 0; i < iInputNeuronsAmount; i++)
                {
                    if ((tCurrentTupple.Result[i] != 1) && (tCurrentTupple.Result[i] != 0))
                    {
                        throw (new Exception("Tupple result array must contain only 1 or 0"));
                    }
                    if (((Neuron)alNeurons[i]).ntType == NeuronType.inputNeuron)
                        ((Neuron)alNeurons[i]).dOutputValue = tCurrentTupple.Result[i];
                }

                //Compare expected result to actual result
                double dActualResult = calculateOutputForCurrentInput();
                int iNormalizedResult = normalizeOutputToBinaryInt(dActualResult);

                alResultsArray.Add(iNormalizedResult);
            }

            return alResultsArray;
            /*
            //Perform testing as long as there are learning tupples
            Tupple tCurrentTupple;
            ArrayList alResultsArray = new ArrayList();

            while ((tCurrentTupple = dbInputDB.getNextTestTupple()) != null)
            {
                //Populate the input neurons with the current tupple input
                for (int i = 0; i < iInputNeuronsAmount; i++)
                {
                    if ((tCurrentTupple.Result[i] != 1) && (tCurrentTupple.Result[i] != 0))
                    {
                        throw (new Exception("Tupple result array must contain only 1 or 0"));
                    }
                    if (((Neuron)alNeurons[i]).ntType == NeuronType.inputNeuron)
                        ((Neuron)alNeurons[i]).dOutputValue = tCurrentTupple.Result[i];
                }

                //Compare expected result to actual result
                double dActualResult = calculateOutputForCurrentInput();

                alResultsArray.Add(normalizeOutputToBinaryInt(dActualResult));
            }
            return alResultsArray;
            */
        }

        /*Converts double to 1 or 0 according to rounding function
         * Returns - 1 / 0
         * Throws exception when value can not be rounded to 1/0
         */ 
        int normalizeOutputToBinaryInt(double value)
        {
            int normalizedOutput = (int)Math.Round(value);

            //Verify that value is valid
            if ((normalizedOutput != 0) && ((int)Math.Round(value) != 1))
                throw (new Exception("Neuron output " + value + " can not be rounded to 1 or 0"));

            return normalizedOutput;
        }
    }
}
